【LINEミニアプリ】アプリ内の特定の属性を持つ未会員ユーザーを識別する方法を考えてみた
リテールアプリ共創部のるおんです。
LIFFアプリの開発において、アプリ内で特定の属性を持つ会員登録したユーザーと未会員ユーザーを識別する必要性に直面しました。
これまでは会員登録したユーザーのLINE UIDを取得し、そのユーザーの属性に応じてLINE OAMでオーディエンスを作成し、会員向けにメッセージ配信を行なっていました。
しかし、これだと会員登録をしてくれなかったユーザーに対してメッセージ配信などのアクションを実行することができませんでした。また、特定の属性を持つ未会員ユーザーにのみメッセージを配信したかったため、単純に全友達の中から会員を除くユーザーにメッセージ配信をするだけではこの要求に応えられません。
今回はこの課題に対する解決策を考案し実装してみましたので、その方法をご紹介します。
やりたいこと
特定の属性を持つ未会員ユーザーにのみメッセージを配信したい。
我々が開発しているLINEミニアプリでは、アプリの初期アクセス画面においてクエリパラメーターで流入経路を取得するようにしていました。
例)
カフェA: https://miniapp.line.me/xxxxxxxxxxx/cafe=A
カフェB: https://miniapp.line.me/xxxxxxxxxxx/cafe=B
カフェAからLINEミニアプリを追加してくれた未会員ユーザーと、カフェBからLINEミニアプリを追加してくれた未会員ユーザーに対してそれぞれメッセージを出し分けることが今回のゴールです。
これらの課題を解決するために、以下のアプローチを採用しました。
会員テーブルとは別に、一度でもアクセスしたユーザー全員を保存する専用テーブル(Guestテーブル)を作成し、そのテーブルにLINE UIDや名前、流入経路 の情報を格納するのに加えて、会員か未会員かの ステータス を持たせるようにしました。
イメージとしては、以下の図のようなものです。
このようにすることで、Guestテーブルの流入経路ごとに、isMember
ステータスがfalse
のユーザー群のLINE UIDを使用することで、未会員ユーザーのみに特定アクション(メッセージ一斉配信など)をすることができます。
例えば、以下のように カフェAの未会員ユーザー に対してメッセージを配信したい場合は、1列目と5列目のTAROさんとJIROさんのUIDを使ってオーディンエンスを作成すれば該当ユーザーに対してのみ一斉にメッセージを配信できます。
uid | name | cafe | isMember |
---|---|---|---|
U111111 | TARO | カフェA | false |
U222222 | MARIO | カフェB | true |
U333333 | HANAKO | カフェB | false |
U444444 | HANAO | カフェA | true |
U55555 | JIRO | カフェA | false |
実際にやってみた
ではこの構成を実際に実装してみました。フロントエンドにReact、バックエンドにAWS LambdaをNode.jsを使って実装します。データベースはAmazon DynamoDBを用いています。
以下は簡単な手順です。
- 専用テーブル作成
すべてのLIFFアプリアクセスユーザーの情報を保存するテーブル(Guestテーブル)を新設します。 - 初回アクセス時の情報送信(フロントエンド)
アプリの初期表示画面にて、クエリパラーメーターから 流入経路の情報 ととユーザー情報を取得するための アクセストークン をバックエンドに送信します。 - ユーザー情報の保存(バックエンド)
受け取った流入経路とアクセストークンからユーザー情報を取得し、Guestテーブルに保存する。
会員登録をする際にisMember
ステータスを更新。
インフラストラクチャはAWS CDKを用いてサクッと作ってみました。
CDK
linUserIdをパーティションキーとするDynamoDBのGuestテーブルと、保存処理をするためのLambda、エンドポイントとしてのAPI Gatewayを作成しています。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as aws_dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { RemovalPolicy } from 'aws-cdk-lib';
export class GuestLiffTestStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
/**
* GuestUserを登録するLambda
*/
const registerGuestUserFn = new nodejs.NodejsFunction(this, "registerGuestUserFn", {
entry: "server/handler/registerGuestUserHandler.ts",
runtime: lambda.Runtime.NODEJS_20_X,
functionName: "registerGuestUserFn",
description: "アクセスしたユーザーをすべて保存するLambda関数",
architecture: lambda.Architecture.ARM_64,
});
/**
* ゲスト用のAPI Gateway
*/
const api = new apigateway.LambdaRestApi(this, "registerGuestUserApi", {
handler: registerGuestUserFn,
proxy: false,
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
allowMethods: apigateway.Cors.ALL_METHODS,
allowHeaders: apigateway.Cors.DEFAULT_HEADERS,
statusCode: 200,
},
});
const registerGuestUserIntegration = new apigateway.LambdaIntegration(registerGuestUserFn);
api.root.addMethod("POST", registerGuestUserIntegration)
/**
* ゲスト用のDynamoDB
*/
const guestTable = new aws_dynamodb.Table(
this,
"guestTable",
{
partitionKey: {
name: "lineUserId",
type: aws_dynamodb.AttributeType.STRING,
},
billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: RemovalPolicy.RETAIN,
tableName: "Guest",
pointInTimeRecovery: true,
},
);
guestTable.grantReadWriteData(registerGuestUserFn);
}
}
フロントエンド
ユーザーが最初にアクセスするのが以下のような画面だとします。
この画面を開いた瞬間、useEffect
を使用してアクセストークンをバックエンドに送信し、ゲストユーザーを保存するエンドポイントを叩きます。
import { useEffect, useState } from "react";
import liff from "@line/liff";
import "./App.css";
import axios from "axios";
function App() {
// 省略
const registerGuestUser = async () => {
// クエリパラメーターからcafeIdを取得
const cafeId = new URLSearchParams(window.location.search).get("cafeId");
// LINEユーザーのアクセストークンを取得
const accessToken = await liff.getAccessToken();
if (cafeId && accessToken) {
await axios.post(
"https://ihyybtxzsc.execute-api.ap-northeast-1.amazonaws.com/prod", // サーバーサイドへのリクエストエンドポイント
{ accessToken, cafeId }
);
}
};
+ // 初回アクセス時に実行
+ useEffect(() => {
+ registerGuestUser();
+ }, []);
return (
<div className="App">
<h1>アプリへようこそ!</h1>
<a href="https://xxxxxxxx/">
会員登録はこちらから
</a>
</div>
);
}
export default App;
フロント側でユーザー情報を取得してバックエンドに送信するのではなく、必ずアクセストークンやIDトークンなどを送信するようにしてください。詳しくはこちらの記事で解説しています。
バックエンド
次に、バックエンドを実装します。フロントから送られてきたアクセストークンを使用してユーザー情報を取得し、流入経路とisMember
ステータスをfalse
としてデータベースに保存します。
以下はLambda関数の例です。
import axios from "axios";
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb';
// DynamoDBのクライアントの初期化
const dynamoDB = new DynamoDBClient({ region: 'ap-northeast-1' });
const dynamoDBDocumentClient = DynamoDBDocumentClient.from(dynamoDB);
export const handler = async (event: any) => {
const { accessToken, cafeId } = JSON.parse(event.body);
// LINEユーザー情報を取得
const userInfoResponse = await axios.get("https://api.line.me/v2/profile", {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
const lineUserId = userInfoResponse.data.userId;
// DynamoDBからユーザーを取得
const guestUser = await dynamoDBDocumentClient.send(new GetCommand({
TableName: 'Guest',
Key: {
lineUserId: lineUserId
}
}));
if (!guestUser.Item) {
// ゲストが存在しない場合、新規作成
+ await dynamoDBDocumentClient.send(new PutCommand({
+ TableName: 'Guest',
+ Item: {
+ lineUserId: lineUserId,
+ lineDisplayName: userInfoResponse.data.displayName,
+ cafeId: cafeId,
+ isMember: false // ステータスをfalseにして、未会員ユーザーとして識別できるようにする
+ }
}));
return {
statusCode: 201,
body: JSON.stringify({ message: "新しいユーザー情報を保存しました" })
};
} else {
// ユーザーが既に存在する場合
return {
statusCode: 200,
body: JSON.stringify({ message: "ユーザーは既に登録されています" })
};
}
};
実際にこのアプリにアクセスしてみると、ユーザー情報が保存されていることが確認できます。
これで未会員のユーザー情報を取得できるようになりました。
では、次に会員登録をした際にはこのisMember
ステータスをtrueにして、会員登録済みかどうかをわかるようにします。今回は実装していませんが、すでに会員登録処理がある前提で進めます。
// 会員情報の登録の際に、Guestテーブルの該当ユーザーのisMemberステータスを更新する
await dynamoDBDocumentClient.send(new UpdateCommand({
TableName: 'Guest',
Key: {
lineUserId: userInfoResponse.data.userId
},
UpdateExpression: 'SET isMember = :isMember',
ExpressionAttributeValues: {
+ ':isMember': true
},
ReturnValues: 'UPDATED_NEW'
}));
// 会員情報を登録する処理
// 省略
return {
statusCode: 200,
body: JSON.stringify({ message: "会員情報を保存しました" })
};
};
会員登録をすると同時に、GuestテーブルのisMember
ステータスがtrue
になっていることが確認できます。
このようにすることで、利用ユーザー数が増えた場合に、ユーザーの属性と、未会員ユーザーなのか会員登録済みユーザーなのかを識別することができますね。
例えば、カフェAかつ、未会員のユーザーは以下の赤線内の二人であることがわかります。
そして、この2人のLINE UIDを用いてオーディエンスを作成し、メッセージを配信するなどのアクションをすることが可能になります。
おわりに
今回、LIFFアプリにおける特定の属性の会員・未会員ユーザーの識別方法について、具体的な実装例を交えて紹介しました。この手法を用いることで、特定の流入経路や、ユーザーの属性を持つ未会員ユーザーに特化したメッセージ配信や施策を実施できるようになり、会員登録率の向上につながる可能性があります。
以上、どなたかの参考になれば幸いです。